//
//  Reachability.swift
//  CleanMan
//
//  Created by whale on 2022/9/9.
//

import Foundation
import SystemConfiguration

class Reachability {
    var hostname: String?
    var isRunning = false
    var isReachableOnWWAN: Bool
    var reachability: SCNetworkReachability?
    var reachabilityFlags = SCNetworkReachabilityFlags()
    let reachabilitySerialQueue = DispatchQueue(label: "ReachabilityQueue")
    init?(hostname: String) throws {
        guard let reachability = SCNetworkReachabilityCreateWithName(nil, hostname) else {
            throw Network.Error.failedToCreateWith(hostname)
        }
        self.reachability = reachability
        self.hostname = hostname
        isReachableOnWWAN = true
    }
    init?() throws {
        var zeroAddress = sockaddr_in()
        zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
        zeroAddress.sin_family = sa_family_t(AF_INET)
        guard let reachability = withUnsafePointer(to: &zeroAddress, {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                SCNetworkReachabilityCreateWithAddress(nil, $0)
            }}) else {
                throw Network.Error.failedToInitializeWith(zeroAddress)
        }
        self.reachability = reachability
        isReachableOnWWAN = true
    }
    var status: Network.Status {
        return  !isConnectedToNetwork ? .unreachable :
                isReachableViaWiFi    ? .wifi :
                isRunningOnDevice     ? .wwan : .unreachable
    }
    var isRunningOnDevice: Bool = {
#if targetEnvironment(simulator)
            return false
        #else
            return true
        #endif
    }()
    deinit { stop() }
}

extension Reachability {
    func start() throws {
        guard let reachability = reachability, !isRunning else { return }
        var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
        context.info = Unmanaged<Reachability>.passUnretained(self).toOpaque()
        guard SCNetworkReachabilitySetCallback(reachability, callout, &context) else { stop()
            throw Network.Error.failedToSetCallout
        }
        guard SCNetworkReachabilitySetDispatchQueue(reachability, reachabilitySerialQueue) else { stop()
            throw Network.Error.failedToSetDispatchQueue
        }
        reachabilitySerialQueue.async { self.flagsChanged() }
        isRunning = true
    }
    func stop() {
        defer { isRunning = false }
        guard let reachability = reachability else { return }
        SCNetworkReachabilitySetCallback(reachability, nil, nil)
        SCNetworkReachabilitySetDispatchQueue(reachability, nil)
        self.reachability = nil
    }
    var isConnectedToNetwork: Bool {
        return isReachable &&
               !isConnectionRequiredAndTransientConnection &&
               !(isRunningOnDevice && isWWAN && !isReachableOnWWAN)
    }
    var isReachableViaWiFi: Bool {
        return isReachable && isRunningOnDevice && !isWWAN
    }

    /// 此flags表示网络节点或地址是否可达，包括是否需要链接以及建立连接时是否需要用户干预
    var flags: SCNetworkReachabilityFlags? {
        guard let reachability = reachability else { return nil }
        var flags = SCNetworkReachabilityFlags()
        return withUnsafeMutablePointer(to: &flags) {
            SCNetworkReachabilityGetFlags(reachability, UnsafeMutablePointer($0))
            } ? flags : nil
    }

    /// 比较当前的flags和之前的flags，如果有改变，发送一个falgsChanged通知
    func flagsChanged() {
        guard let flags = flags, flags != reachabilityFlags else { return }
        reachabilityFlags = flags
        NotificationCenter.default.post(name: .flagsChanged, object: self)
    }

    /// 指定的节点或地址瞬态连接可达，如PPP。
    var transientConnection: Bool { return flags?.contains(.transientConnection) == true }

    /// 指定的节点或地址使用当前的网络配置连接可达
    var isReachable: Bool { return flags?.contains(.reachable) == true }

    /// 指定的节点或地址使用当前的网络配置连接可达，但必须先建立连接。 如果此flag被设置，kSCNetworkReachabilityFlagsConnectionOnTraffic flag, kSCNetworkReachabilityFlagsConnectionOnDemand flag
//, 或kSCNetworkReachabilityFlagsIsWWAN flag也需要设置来指示必须连接的类型。如果需要手动连接， kSCNetworkReachabilityFlagsInterventionRequired flag 也需要设置.
    var connectionRequired: Bool { return flags?.contains(.connectionRequired) == true }

    /// 指定的节点或地址使用当前的网络配置连接可达，但必须先建立连接。指向该名称或地址的任何流量将启动连接。
    var connectionOnTraffic: Bool { return flags?.contains(.connectionOnTraffic) == true }

    /// 指定的节点或地址使用当前的网络配置连接可达，但必须先建立连接。
    var interventionRequired: Bool { return flags?.contains(.interventionRequired) == true }

    /// 指定的节点或地址使用当前的网络配置连接可达，但必须先建立连接。使用CFSocketStream接口按需建立连接（有关此信息，请参阅CFStream Socket Additions）。
    var connectionOnDemand: Bool { return flags?.contains(.connectionOnDemand) == true }

    /// 指定节点或地址与当前系统网络接口相关联。
    var isLocalAddress: Bool { return flags?.contains(.isLocalAddress) == true }

    /// 指定节点或地址的网络流量将不会通过网关，而是直接路由到系统中的接口。
    var isDirect: Bool { return flags?.contains(.isDirect) == true }

    /// 指定节点或地址4G可达
    var isWWAN: Bool { return flags?.contains(.isWWAN) == true }

    /// 指定的节点或地址使用当前的网络配置连接可达，但必须先建立连接。如果此标识被设置。
    /// 指定节点或地址瞬态连接可达，例如PPP
    var isConnectionRequiredAndTransientConnection: Bool {
        return (flags?.intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection]) == true
    }
}

func callout(reachability: SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutableRawPointer?) {
    guard let info = info else { return }
    DispatchQueue.main.async {
        Unmanaged<Reachability>.fromOpaque(info).takeUnretainedValue().flagsChanged()
    }
}

extension Notification.Name {
    static let flagsChanged = Notification.Name("FlagsChanged")
}

struct Network {
    static var reachability: Reachability?
    enum Status: String, CustomStringConvertible {
        case unreachable, wifi, wwan
        var description: String { return rawValue }
    }
    enum Error: Swift.Error {
        case failedToSetCallout
        case failedToSetDispatchQueue
        case failedToCreateWith(String)
        case failedToInitializeWith(sockaddr_in)
    }
}
